package org.Noolab.exec;



import java.io.IOException;
import java.util.*;

import org.NooLab.utilities.logging.PrintLog;
import org.Noolab.exec.auth.AuthCheck;
import org.Noolab.exec.auth.AuthCheckIntf;
import org.Noolab.exec.gluex.Gluex;
import org.Noolab.exec.net.ServerMsgHandler;
import org.Noolab.exec.net.ServerMsgHandlerIntf;
import org.Noolab.exec.net.TcpMessageHandlerFactory;
import org.Noolab.exec.net.TcpServer;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;
 

/**
 * 
 * 
 * Web daemon thread.  Creates a TCP socket on port 8008 (default) and listens on it forever.
 * For each client request, creates a proxy thread to handle the request.
 * 
 * This should be started with the OS...
 * 
 * The purpose is to run instances of applications in the VM , 
 * using the "Executor" class from org.apache.commons.exec.*
 * 
 * parameter is port, if none then port -> 8008
 *
 * the daemon understands the following commands
 * 
 * - stop itself
 * - create participant Type, N 
 * - stop participant type, all, last
 * 
 * the exchange format is propriety:
 *  - <>#$[GUID]</> 
 *  - <>#create::T=[R,S,B]::N=1<[GUID]/>
 *  - <>#create::T=[R,S,B]::N=1</>
 *  - <>#stop::T=[R,S,B]::ALL::LAST</> 
 *  - <>#exit</> 
 *
 * a minimum level of security is built in 
 *   (instead of more complicated public/shared key procedure):
 *   
 * 1. any client sends a first GUID
 * 2. then, after a few moments, it send a second one,
 *    this GUID then, either of two procedures will be applied 
 *    - the most frequent char changes positions with second most frequent char
 *    - the last position of the most frequent char will exchange position with its left neighbor
 *    selection of alternatives is by odd of the char value of the most frequent char 
 * 3. the server in the daemon will check the returned processed GUID
 * 4. the Guid is valid only 2 minutes, or 5 actions
 * 5. IP addresses which fail to behave correctly will not be accepted any more
 * 
 * 
 * stop is issued e.g. by a MessageBoard that is shutting down
 * 
 */
public class Daemon implements Runnable, Observer{

	TcpServer tcpServer;
	int listenPort; 
	Observer observer;
	ServerMsgHandlerIntf serverMsg ; 
	String description="";
	TcpMessageHandlerFactory msgfactory;
	
	Thread dthrd;
	
	Map<String,String> activeGUID  = new HashMap<String,String>();
	Map<String,String> waitingGUID = new HashMap<String,String>();
	Map<String,Long> activationsT  = new HashMap<String,Long>();
	Map<Integer,String> registeredModules = new HashMap<Integer,String>();
	
	
	Thread exithook ;
	private ExitCleaner cleaner ;
	
	
	PrintLog out = new PrintLog(2,true);
	
	public Daemon( int port ) {
		listenPort = port ;
		
		dthrd = new Thread(this,"daemon thread");
		dthrd.start() ;
		
	}

	private void init(){
		
		
		msgfactory = new TcpMessageHandlerFactory() ;
		serverMsg = new ServerMsgHandler(msgfactory); 
		
		tcpServer = new TcpServer( listenPort, 
				  				   this, serverMsg, 
				  				   description ) ;
		tcpServer.activate() ;
		System.out.println("TCP server started and listening on port "+listenPort) ;
		
		cleaner = new ExitCleaner(this);
		
		exithook = new Thread( getShutdownRunnable() );
        Runtime.getRuntime().addShutdownHook(exithook);
	}
	 

	@Override
	public void update(Observable arg0, Object arg1) {
		
		String str;
		
		str = (String)arg1 ;
		
		// PrimaryMessageHandler msghandler = 
		new PrimaryMessageHandler( str );
		
	}
	
	public void run() {
		init();
	}
	
	

    public Runnable getShutdownRunnable(){
        return cleaner;
    }
    
    protected void cleanOnExit(){
    	System.out.println("sending out shutdown requests...");
    }
	
	public void stopTCPserver(){
		String guidStr = "";
		int port ;
		
		tcpServer.stopServer();
		// send a shutdown message to all known modules 
		
		for (Map.Entry<Integer,String> entry : registeredModules.entrySet()) {
        	// System.out.println("Key : " + entry.getKey() + " Value : " + entry.getValue());
        	// creates a shutdown XML
			guidStr = entry.getValue();
			port = entry.getKey() ;
			
			(new Gluex()).sendShutdownMessage(port,guidStr) ; 
        } // ->
		
	}
	 
	protected String extractGuidFromMsg(String msgstr){
		// #create::T=[R,S,B]::N=1<[GUID]/>
		String guidStr="",str;
		int p;
		
		p = msgstr.lastIndexOf("<");
		str = msgstr.substring(p+1,msgstr.length()) ;
		
		guidStr = str.replace("/>", "");
		 
		return guidStr;
	}
	
	/** 
	 * any incoming message will be handled separately:
	 * - checking map (ID::obj)
	 * - basic auth calculations on Guid for acceptance check
	 * 
	 *
	 */
	class PrimaryMessageHandler{
		
		AuthCheckIntf authCheck;
		String currentMsg ;
		
		
		public PrimaryMessageHandler(String msgstr){
			String str,str3s="",guid1="", guid2="";
			
			boolean hb, wp;
			currentMsg = msgstr.trim();
			
			if (msgstr.startsWith("<$>#")){
				// the started module returns the GUID, which was provided as a startup argument
				// <$>#GUID=9509c499-98df-47e4-82a4-5c24fd90fc7d::PORT=7004</>
				// so we know the started GUID and the port,
				// when we shut down , we send shut down messages to all modules too!
				System.out.println("CCI: "+msgstr) ;
				
				str = msgstr.replace("<$>#", "").replace("</>", "");
				// get guid ,get port + put it to a map
				(new RegParser(str)).go();
				
				return;
			}
			if (msgstr.startsWith("<>#$")){
				str = msgstr.replace("<>#$", "").replace("</>", "");
				System.out.println("MID: "+str) ;
				
				str3s = str.substring(0,4);
				wp = waitingGUID.containsKey(str3s);
				 
				if (wp ==false){
					waitingGUID.put(str3s, str);
				}else{
					guid1 = waitingGUID.get(str3s) ;
					guid2 = str ;
					
					waitingGUID.remove(str3s);
					authCheck = (new AuthCheck());
					hb = authCheck.checkByGuid(guid1, guid2);
					if (hb){
						activeGUID.put( guid2,guid1 ); // the second first !!
						activationsT.put( guid1, System.currentTimeMillis());
					}
				}
			} // is GUID ?
			else{
				if (msgstr.startsWith("<>#")){
					str = msgstr.replace("<>#", "").replace("</>", "");
					 
					System.out.println("CMD: "+str) ;
					
					guid1 = "";
					guid2 = extractGuidFromMsg(str);
					
					if (activeGUID.containsKey(guid2)){
						guid1 = activeGUID.get(guid2) ;
					}
					
					if (guid1.length()>0){
						str = str.replace("<"+guid2+"/>", "");
						CmdParser cmdParser = (new CmdParser(str));
						cmdParser.go(guid2);
					}
				} // is a command ?
			} // not a guid ?
			
			
			
		}
		
	}
	
	@SuppressWarnings("static-access")
	private void delay(int millis){
		try{
			Thread.currentThread().sleep(millis);
			
		}catch(Exception e){ }
	}
	
	
	
	class RegParser{
		String cmdstr, guidStr ="";
		int port=0;
		
		// #GUID=9509c499-98df-47e4-82a4-5c24fd90fc7d::PORT=7004
		public RegParser( String cmdstr){
			this.cmdstr = cmdstr;
			parse();
		}
		
		private void parse(){
			String[] parts, pstr;
			String str;
			
			parts = cmdstr.split("::");
			
			str = parts[0];
			guidStr = str.split("=")[1] ;
			
			str = parts[1];
			str = str.split("=")[1] ;
			port = Integer.parseInt(str) ;
		}
		
		public void go(){

			if ((guidStr.length()>0) && (port>0)){
				registeredModules.put(port,guidStr);
			}
			
		}
		
	}
	
	
	class CmdParser{
		/* the exchange format is propriety:
		*  - <>#$[GUID]</> 
		*  - <>#create::T=[R,S,B]::N=1</>
		*  - <>#stop::T=[R,S,B]::ALL::LAST</> 
		*  - <>#install::<c>[BASE64 ENCODED TAR PACKAGE]</c></> 
		*  - <>#exit</>
		*/
		String cmdstr;
		
		public CmdParser( String cmdstr){
			this.cmdstr = cmdstr;
		}
		
		public void go(String guid){
			
			if (cmdstr.startsWith("create::")){
				
				new CreateInstance(cmdstr).go(guid);
			}
			if (cmdstr.startsWith("stop::")){
				new StopInstance().go();
			}
			if (cmdstr.startsWith("install::")){
				new InstallGlue().go();
			}
			if (cmdstr.startsWith("exit::")){
				stopTCPserver(); 
				System.exit(0) ;
			}
			
		}
		
		
		
	} // class CmdParser

	class CreateInstance{
		String cmdstr;
		int count=0;
		String typeID="";
		
		InstanceExecHandler instance;
		
		public CreateInstance(String cmdstr){
			this.cmdstr = cmdstr;
			parse();
			 
		}
		
		private void parse(){
			// create::T=[R,S,B]::N=1
			String str, pstr;
			String[] cparts = cmdstr.split("::") ;
			
			for (int i=1;i<cparts.length;i++){
				str = cparts[i];
				if (str.startsWith("T=")){
					typeID = str.split("=")[1];
				}
				if (str.startsWith("N=")){
					pstr= str.split("=")[1];
					count = Integer.parseInt(pstr);
				}
				
			} // i-> all parts
		}
		
		public InstanceExecHandler go(String guid){
			String cs="", path="", jarfile;
			instance = null;
			
			
			for (int i=0;i<count;i++){
					cs = (i+1)+"th";
					if (i+1==1){ cs = (i+1)+"st";} 
					if (i+1==2){ cs = (i+1)+"nd";} 
					if (i+1==3){ cs = (i+1)+"rd";} 
					System.out.println("create "+cs+" instance of type "+typeID);
				
				try {
					path = "D:/dev/processing/glueDemo/_install_libraries/NooLabGlue/library/";
					jarfile = "receptor.jar" ;
					instance = execCreate(guid, path, jarfile);
					delay(4000);
				} catch (IOException e) {
					e.printStackTrace();
				}
				
			}
			return instance;
		}
		
		
		private InstanceExecHandler execCreate( String processGuid, 
												String path,
												String jarfile ) throws IOException {
			 
			int exitValue;
		    ExecuteWatchdog watchdog = null;
		    
		    InstanceExecHandler execHandler;
		    CommandLine commandLine ;
		    Executor executor; 
		    
		    									
		    String pdeExec = "java";
		    commandLine = new CommandLine(pdeExec); // the java jar call
		    commandLine.addArgument("-jar");
		    commandLine.addArgument( path+jarfile);
		    commandLine.addArgument("\""+processGuid+"\"");
		    								out.print(2, "...just before start of '"+jarfile+"' ...");
		    executor = new DefaultExecutor();
		    executor.setExitValue(0);
		    
		    // pass a "ExecuteResultHandler" for non-blocking execution
		      
		       System.out.println("[run] Executing non-blocking start of module  ...");
		       execHandler = new InstanceExecHandler(watchdog);
		       executor.execute(commandLine, execHandler);  
		       								out.print(2, "...executor has been returning : ");
		    return execHandler;
		} // execCreate

	}
	
	class StopInstance{
		
		public void go(){
			
		}
	}
	
	class InstallGlue{
		
		public void go(){
			
		}
	}
	
	
	private static class ExitCleaner implements Runnable {
		private Daemon daemon;

		private ExitCleaner(Daemon daemon) {
			this.daemon = daemon;
		}

		public void run() {
			try {
				daemon.cleanOnExit();
			} catch (Exception ce) {

				ce.printStackTrace();
			}
		}
	}

}

	
 
class InstanceExecHandler extends DefaultExecuteResultHandler {

	private ExecuteWatchdog watchdog;

	public InstanceExecHandler(ExecuteWatchdog watchdog) {
		this.watchdog = watchdog;
	}

	public InstanceExecHandler(int exitValue) {
		super.onProcessComplete(exitValue);
		
	}

	
	public void onProcessComplete(int exitValue) {
		super.onProcessComplete(exitValue);
		System.out.println("[resultHandler] The module was successfully started ...");
	}

	public void onProcessFailed(ExecuteException e) {
		super.onProcessFailed(e);
		if (watchdog != null && watchdog.killedProcess()) {
			System.err.println("[resultHandler] The process hangs");
		} else {
			System.err.println("[resultHandler] The process failed : " + e.getMessage());
		}
	}
}
